package com.emeraldparser.parse;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import com.emeraldparser.parse.AbstractParser.Node;
import com.emeraldparser.symbol.Symbol;
import com.emeraldparser.util.Graph.Edge;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

@SuppressWarnings("serial")
public class ParseVisualizationPanel extends JComponent implements AbstractParser.Listener {

	private static final long DELAY_IN_MILLISECONDS = 0;
	private static final int WIDTH = 400;
	private static final int BOTTOM_OFFSET = 200;

	private final AbstractParser parser;
	private final Multimap<Integer, Node> nodesByIteration = HashMultimap.create();

	public ParseVisualizationPanel(AbstractParser parser) {
		this.parser = parser;
		parser.addListener(this);
	}

	@Override
	public Dimension getPreferredSize() {
		Set<Integer> iterations = Sets.newHashSet();
		
		for (Node node : parser.parseGraph.vertices()) {
			iterations.add(node.iteration);
		}
		
		int minIteration = Collections.min(iterations);
		int maxIteration = Collections.max(iterations);
		
		int iterationCount = maxIteration - minIteration + 1;
		
		return new Dimension( (iterationCount + 1) * WIDTH, 400 );
	}

	@Override
	public void paintComponent(Graphics g) {
		Graphics2D graphics = (Graphics2D) g;
		
		int min = minIteration();
		int max = maxIteration();
		
		nodesByIteration.clear();
		
		for (Node node : parser.parseGraph.vertices()) {
			nodesByIteration.put(node.iteration, node);
		}
		
		for (int iteration = min; iteration <= max; iteration++) {
			int x = 0;
			for (Node node : nodesByIteration.get(iteration)) {
				x = xPosition(node);
			}
			
			graphics.setColor(Color.LIGHT_GRAY);
			graphics.drawLine(x, 0, x, getHeight());
		}
		
		graphics.setColor(Color.BLACK);
		graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		
		for (Edge<Node, Symbol<?>> edge : Lists.newArrayList(parser.parseGraph.edges())) {
			Point source = position(edge.getSource());
			Point destination = position(edge.getDestination());
			graphics.drawLine(source.x, source.y, destination.x, destination.y);
			Graphics2D subGraphics = (Graphics2D) graphics.create();
			try {
				subGraphics.translate(source.x, source.y);
				subGraphics.rotate(Math.tan( (double) (destination.y - source.y) / (double) (destination.x - source.x) ));
				String string = String.valueOf(edge.getLabel());
				int width = subGraphics.getFontMetrics().stringWidth(string);
				double distance = Math.sqrt( Math.pow((destination.y - source.y), 2) + Math.pow((destination.x - source.x), 2) );
				subGraphics.translate((int) Math.round( (distance - width) / 2 ), 0);
				subGraphics.drawString(string, 0, 0);
			} finally {
				subGraphics.dispose();
			}
		}
		
		Iterator<Symbol<?>> i = parser.input.iterator();
		for (int iteration = min; iteration <= max; iteration++) {
			int x = 0;
			for (Node node : nodesByIteration.get(iteration)) {
				x = xPosition(node);
				int y = yPosition(node);
				if (parser.parseGraph.getAcceptVertices().contains(node)) {
					graphics.setColor(Color.GREEN);
				} else {
					graphics.setColor(Color.BLACK);
				}
				graphics.fillOval(x - 5, y - 5, 10, 10);
				graphics.drawString(node.state.toString(), x + 10, y);
			}
			
			graphics.setColor(Color.BLACK);
			graphics.drawString(String.valueOf(iteration), x + 5, getHeight() - BOTTOM_OFFSET + 50);
			
			Symbol<?> symbol = i.next();
			
			Font oldFont = graphics.getFont();
			try {
				graphics.setFont(new Font(oldFont.getName(), Font.BOLD, oldFont.getSize()));
				String sym = String.valueOf(symbol);
				int symwidth = graphics.getFontMetrics().stringWidth(sym);
				graphics.drawString(String.valueOf(symbol), x + ((WIDTH - symwidth) / 2), getHeight() - BOTTOM_OFFSET + 50);
			} finally {
				graphics.setFont(oldFont);
			}
		}
	}

	private Point position(Node node) {
		List<Node> nodes = Lists.newArrayList(nodesByIteration.get(node.iteration));
		
		Collections.sort(
				nodes,
				new Comparator<Node>() {
					@Override
					public int compare(Node o1, Node o2) {
						// TODO Auto-generated method stub
						Integer pos1 = parser.orderedNodes.indexOf(o1);
						Integer pos2 = parser.orderedNodes.indexOf(o2);
						return pos1.compareTo(pos2);
					}
				}
			);
		
		int x = (int) Math.round( (node.iteration + 0.5) * WIDTH );
		int y = getHeight() - BOTTOM_OFFSET - (nodes.indexOf(node) * 50);
		return new Point(x, y);
	}

	private int xPosition(Node node) {
		return position(node).x;
	}

	private int yPosition(Node node) {
		return position(node).y;
	}

	private Collection<Integer> iterations() {
		return Sets.newHashSet(
				Collections2.transform(
						parser.parseGraph.vertices(),
						new Function<Node, Integer>() {
							@Override
							public Integer apply(Node node) {
								return node.iteration;
							}
						}
					)
			);
	}

	private int minIteration() {
		return Collections.min(iterations());
	}

	private int maxIteration() {
		return Collections.max(iterations());
	}

	@Override
	public void update() {
		// TODO Auto-generated method stub
		try {
			SwingUtilities.invokeAndWait(
					new Runnable() {
						@Override
						public void run() {
							repaint();
						}
					}
				);
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (InvocationTargetException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		
		try {
			Thread.sleep(DELAY_IN_MILLISECONDS);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}
